//
//  PeripheralManager.swift
//  Pods
//
//  Created by tom on 2025/9/19.
//
import CoreBluetooth
import Flutter
import IOS_BLE_Library
import Combine



typealias ProgressCallback = (Int, Int) -> ()

class PeripheralManager:ProvidePeripheralEventStreamHandler{
    private var eventSink: PigeonEventSink<any PeripheralEvent>? = nil
    
    
    private lazy var centralManager = CentralManager()
    
    let logEventHandler = LogEventHandler()
    
    var blePacketSubjects:[String:PassthroughSubject<BlePacket, Never>] = [String:PassthroughSubject<BlePacket, Never>]()
    
    var dfuProcessTasks:[String:Task<(), Never>] = [String:Task<(), Never>]()
    
    var b1SyncDataProcessTasks:[String:Task<(), Never>] = [String:Task<(), Never>]()
    
    func callSink(event:PeripheralEvent){
        DispatchQueue.main.async {
            self.eventSink?.success(event)
        }
    }
    
    override func onListen(withArguments arguments: Any?, sink: PigeonEventSink<any PeripheralEvent>) {
        eventSink = sink
    }
    
    override func onCancel(withArguments arguments: Any?) {
        eventSink = nil
    }
    
    var peripherals = [String:Peripheral]()
    var disposeBags = [String:Set<AnyCancellable>]()
    
    func getPeripheral(_ address:String)->Peripheral?{
        
        if let peripheral = peripherals[address]{
            return peripheral
        }
        if let uuid = UUID(uuidString: address), let peripheral = centralManager.retrievePeripherals(withIdentifiers: [uuid]).first{
            let p = Peripheral(peripheral: peripheral)
            self.callSink(event: PeripheralStateEvent(address: address, state: peripheral.state.toPeripheralState()))
            var cancellables = Set<AnyCancellable>()
            peripherals[address] = p
            disposeBags[address] = cancellables
            blePacketSubjects[address] = PassthroughSubject()
            return p
        }
        if let uuid = UUID(uuidString: address), let peripheral = centralManager.retrievePeripherals(withIdentifiers: [uuid]).first{
            self.callSink(event: PeripheralStateEvent(address: address, state: peripheral.state.toPeripheralState()))
            let p = Peripheral(peripheral: peripheral)
            var cancellables = Set<AnyCancellable>()
            peripherals[address] = p
            disposeBags[address] = cancellables
            blePacketSubjects[address] = PassthroughSubject()
            return p
        }
        return nil
    }
    
    
    
}

extension PeripheralManager:PeripheralMethod{
    
    
    func ensureInitialized(address: String, completion: @escaping (Result<Bool, any Error>) -> Void) {
        
    }
    
    func connect(address: String, completion: @escaping (Result<Bool, any Error>) -> Void) {
        guard let peripheral = getPeripheral(address) else {
            return
        }
        guard var cancellables = disposeBags[address] else { return }
        self.disposeBags[address]?.forEach { cancel in
            cancel.cancel()
        }
        debugPrint("connect")
        peripheral.peripheralStateChannel
            .sink {[weak self] state in
                self?.callSink(event: PeripheralStateEvent(address: address, state: state.toPeripheralState()))
            }
            .store(in: &cancellables)
        let connectPublisher = centralManager.connect(peripheral.peripheral)
        let setupPublisher = connectPublisher
            .flatMap({ _ in
                peripheral.setup()
            })
            .flatMap { characteristic in
                peripheral.setNotifyValue(true, for: characteristic)
            }
            .tryMap({ isNotifying in
                if isNotifying{
                    return isNotifying
                }else{
                    throw BleError.setNotifyFail
                }
            })
        
        peripheral.updatedCharacteristicValuesChannel
            .prefix(untilCompletionFrom: setupPublisher)
            .filter({ (characteristic, error) in
                characteristic.uuid == NOTIFY_UUID
            })
            .map({ (characteristic, error) in
                characteristic
            })
            .reduce(Data(), {[weak self] initValue, characteristic in
                let value = initValue + (characteristic.value ?? Data())
                if let packet = try? BlePacket(raw: value){
//                    debugPrint("BlePacket:\(packet.cmd),raw:\(packet.raw.hexEncodedString(options: .upperCase, separator: "_") ?? "")")
                    let data:FlutterStandardTypedData? = packet.data == nil ? nil : FlutterStandardTypedData(bytes: packet.data!)
                    self?.callSink(event: PeripheralNotifyEvent(address: address, cmd: packet.cmd, data: data, raw: FlutterStandardTypedData(bytes: packet.raw)))
                    self?.blePacketSubjects[address]?.send(packet)
                    return Data()
                }else{
                    return value
                }
            })
            .sink { completion in
                debugPrint("updatedCharacteristicValuesChannel completion:\(completion)")
            } receiveValue: { characteristic in
                debugPrint("updatedCharacteristicValuesChannel receiveValue:\(characteristic)")
            }.store(in: &cancellables)
        
        
        setupPublisher
            .sink { complete in
                debugPrint("connect completion:\(complete)")
                self.dfuProcessTasks[address]?.cancel()
                self.dfuProcessTasks[address] = nil
                self.b1SyncDataProcessTasks[address]?.cancel()
                self.b1SyncDataProcessTasks[address] = nil
                self.blePacketSubjects[address]?.send(completion: .finished)
                self.blePacketSubjects[address] = PassthroughSubject()
                switch complete{
                case .finished:
                    break
                case .failure(let error):
                    completion(Result.failure(error))
                }
            } receiveValue: {[weak self] isNotifying in
                self?.callSink(event: PeripheralStateEvent(address: address, state: .ready))
                completion(Result.success(true))
                debugPrint("connect receiveValue:\(isNotifying)")
            }.store(in: &cancellables)
        
        peripheral.writtenCharacteristicValuesChannel
            .sink { completion in
                debugPrint("written completion:\(completion)")
            } receiveValue: { (characteristic, error) in
                debugPrint("written receiveValue:\(characteristic),error:\(error)")
            }.store(in: &cancellables)
        
        
        disposeBags[address] = cancellables
        
        
    }
    
    func disconnect(address: String, completion: @escaping (Result<Bool, any Error>) -> Void) {
        guard let peripheral = getPeripheral(address) else {
            return
        }
        guard var cancellables = disposeBags[address] else { return }
        
        centralManager.cancelPeripheralConnection(peripheral.peripheral)
            .sink { completion in
                debugPrint("disconnect completion:\(completion)")
            } receiveValue: { p in
                debugPrint("disconnect receiveValue:\(p.state)")
                self.disposeBags.removeValue(forKey: address)
            }
            .store(in: &cancellables)
        
        
    }
    
    func write(address: String, cmd: CMD, value: FlutterStandardTypedData?, completion: @escaping (Result<Bool, any Error>) -> Void) {
        
        let packet = BlePacket(cmd: cmd, data: value?.data)
        do{
            
            try write(address: address,raw: packet.raw)
            completion(Result.success(true))
        }catch(let error){
            completion(Result.failure(error))
        }
    }
    
    func write(address:String, cmd:CMD, data:Data?, callback:ProgressCallback? = nil) throws{
        let packet = BlePacket(cmd: cmd, data: data)
        try write(address: address, raw: packet.raw)
    }
    
    func write(address:String, raw:Data, callback:ProgressCallback? = nil) throws{
        guard let peripheral = getPeripheral(address) else {
            throw BleError.peripheralNotFound(address: address)
        }
        guard let services = peripheral.services, let service = services.first(where: { service in
            service.uuid == SERVICE_UUID
        }) else {
            debugPrint("serviceNotFound:\(String(describing: peripheral.services))")
            throw BleError.serviceNotFound(uuid: SERVICE_UUID.uuidString)
        }
        guard let characteristics = service.characteristics, let characteristic = characteristics.first(where: { characteristic in
            characteristic.uuid == WRITE_UUID
        }) else {
            debugPrint("characteristicNotFound:\(String(describing: service.characteristics))")
            throw BleError.characteristicNotFound(uuid: WRITE_UUID.uuidString)
        }
        let chunks = raw.chunks(ofCount: 20)
        var sentCount = 0
        for data in chunks {
            peripheral.writeValueWithoutResponse(data, for: characteristic)
            sentCount += data.count
            if let cb = callback{
                cb(sentCount, raw.count)
            }
        }
        debugPrint("write raw:\(raw.hexEncodedString(separator: "_"))")
        
    }
    
    func request(address:String,cmd:CMD, data:Data?,callback:ProgressCallback? = nil) async throws -> BlePacket{
        
        return try await withCheckedThrowingContinuation { continuation in
            var cancellable:AnyCancellable? = nil
            cancellable = self.blePacketSubjects[address]?
                .first { packet in
                    packet.cmd == cmd
                }
                .timeout(.seconds(5), scheduler: DispatchQueue.main)
                .sink(receiveCompletion: { completion in
                    debugPrint("request complete:\(completion)")
                    switch completion{
                        
                    case .finished:
                        if Task.isCancelled{
                            continuation.resume(throwing: CancellationError())
                        }else{
                            continuation.resume(throwing: BleError.requestTimeout)
                        }
                        
                        break
                    case .failure(let error):
                        continuation.resume(throwing: error)
                    }
                    cancellable?.cancel()
                }, receiveValue: { packet in
                    continuation.resume(returning: packet)
                    cancellable?.cancel()
                })
            
            let packet = BlePacket(cmd: cmd, data: data)
            do{
                try write(address: address, raw: packet.raw, callback:callback)
                
            }catch(let e){
                continuation.resume(throwing: e)
            }
        }
    }
    
    func startDFU(address: String, filePath: String, completion: @escaping (Result<Bool, any Error>) -> Void) {
        if self.dfuProcessTasks.contains(where: { (key, value) in
            return key == address
        }) == false{

            dfuProcessTasks[address] = Task{[weak self] in
                guard let self = self else { return }
                let process = DFUProcess(peripheralManager: self, address: address, filePath: filePath)
                do{
                    for try await event in process.processing(){
                        self.callSink(event: event)
                    }
                    completion(Result.success(true))
                }catch{
                    completion(Result.failure(error))
                }
            }
        }
    }
    
    func cancelDFU(address: String, completion: @escaping (Result<Bool, any Error>) -> Void) {
        dfuProcessTasks[address]?.cancel()
        dfuProcessTasks[address] = nil
        completion(Result.success(true))
    }
    
    func startB1SyncData(address: String, filePath: String?, deleteBeforeWrite: Bool?, completion: @escaping (Result<String?, any Error>) -> Void) {
        if self.b1SyncDataProcessTasks.contains(where: { (key, value) in
            return key == address
        }) == false{

            b1SyncDataProcessTasks[address] = Task{[weak self] in
                guard let self = self else { return }
                await withTaskCancellationHandler {
                    let process = B1SyncDataProcess(address: address, filePath: filePath, deleteBeforeWrite: deleteBeforeWrite, peripheralManager: self)
                    do{
                        for try await event in process.processing(){
                            self.callSink(event: event)
                        }
                        completion(Result.success(process.targetFilePath!))
                    }catch{
                        completion(Result.failure(error))
                    }
                } onCancel: {
                    completion(Result.failure(CancellationError()))
                    self.callSink(event: B1SyncDataEvent(address: address, state: .canceled))
                }

                
            }
        }
        

    }
    
    func cancelB1SyncData(address: String, completion: @escaping (Result<Bool, any Error>) -> Void) {
        b1SyncDataProcessTasks[address]?.cancel()
        b1SyncDataProcessTasks[address] = nil
        completion(Result.success(true))
        try? self.write(address: address, cmd: .stopSync, data: nil)
    }
    
    
}
fileprivate let SERVICE_UUID = CBUUID(string: "de5bf728-d711-4e47-af26-65e3012a5dc7")
fileprivate let NOTIFY_UUID = CBUUID(string: "de5bf729-d711-4e47-af26-65e3012a5dc7")
fileprivate let WRITE_UUID = CBUUID(string: "de5bf72a-d711-4e47-af26-65e3012a5dc7")
internal let ASK_CODE_SUCCESS:UInt8 = 0x00

extension Peripheral{
    func setup() -> AnyPublisher<CBCharacteristic, Error>{
        return self.discoverServices(serviceUUIDs: [SERVICE_UUID])
            .tryMap { services -> CBService in
                guard let service = services.first(where: { service in
                    service.uuid == SERVICE_UUID
                })else {
                    throw BleError.serviceNotFound(uuid: SERVICE_UUID.uuidString)
                }
                return service
            }
            .flatMap { service in
                self.discoverCharacteristics(nil, for: service)
            }
            .tryMap { characteristics in
                guard let write = characteristics.first(where: { characteristic in
                    characteristic.uuid == WRITE_UUID
                })else {
                    throw BleError.characteristicNotFound(uuid: WRITE_UUID.uuidString)
                }
                guard let notify = characteristics.first(where: { characteristic in
                    characteristic.uuid == NOTIFY_UUID
                })else {
                    throw BleError.characteristicNotFound(uuid: NOTIFY_UUID.uuidString)
                }
                return notify
            }
            .eraseToAnyPublisher()
    }
    
}


extension CBPeripheralState{
    func toPeripheralState()->PeripheralState{
        switch self{
            
        case .disconnected:
            return .disconnected
        case .connecting:
            return .connecting
        case .connected:
            return .connected
        case .disconnecting:
            return .disconnecting
        @unknown default:
            return .disconnected
        }
    }
}


extension PeripheralManager{
    func dfu(address:String, filePath:String)async throws{
        self.callSink(event: DFUStateEvent(address: address, state: DFUState.start))
        if !FileManager.default.fileExists(atPath: filePath){
            throw BleError.binFileNotFound(filePath: filePath)
        }
        guard let binFileBytes = FileManager.default.contents(atPath: filePath) else { throw BleError.binFileNotFound(filePath: filePath) }
        let fileSize = binFileBytes.count
        debugPrint("DFU fileSize:\(fileSize)")
        let totalTime:Int = Int(ceil(Double(fileSize) / 1024.0))
        var bytesBuilder = Binary()
        bytesBuilder.writeByte(1)
        bytesBuilder.writeInt(Int32(fileSize), littleEndian: true)
        let checksum:Int = binFileBytes.reduce(0, { last, uint8 in
            last + Int(uint8 & 0xff)
        })
        debugPrint("DFU checksum:\(checksum)")
        bytesBuilder.writeInt(Int32(checksum), littleEndian: true)
        guard let data = try? bytesBuilder.getAllBytes() else {
            throw BleError.dfuDataError(msg: "DFU_INIT 出错")
        }
        var resp = try await request(address: address, cmd: .dfuInit, data: Data(data))
        if resp.data?.first != ASK_CODE_SUCCESS{
            throw BleError.dfuDataError(msg: "DFU_INIT response error:\(resp.data?.hexEncodedString(separator: "_"))")
        }
        try Task.checkCancellation()
        let binFileChunks = binFileBytes.chunks(ofCount: 1024)
        var totalSentSize = 0
        
        
        for (index, item) in binFileChunks.enumerated(){
            try Task.checkCancellation()
            var binary = Binary()
            binary.writeInt(Int16(index + 1), littleEndian: true)
            binary.writeBytes([UInt8](item))
            resp = try await request(address: address, cmd: .dfuPacket, data: Data(binary.getAllBytes()), callback: {[weak self] sent, total in
                debugPrint("DFU Progress:\(sent),\(total)")
                
                self?.callSink(event:
                                DFUStateEvent(
                                    address: address,
                                    state: DFUState.progress,
                                    sentSize: Int64(totalSentSize + sent),
                                    totalSize: Int64(fileSize)
                                )
                )
            })
            if resp.data?.first != ASK_CODE_SUCCESS{
                throw BleError.dfuDataError(msg: "CMD.DFU_PACKET response error:\(resp.data?.hexEncodedString(separator: "_"))")
            }
            totalSentSize += item.count - 6
            try Task.checkCancellation()
        }
        try Task.checkCancellation()
        callSink(event:
                    DFUStateEvent(
                        address: address,
                        state: DFUState.progress,
                        sentSize:Int64(fileSize),
                        totalSize: Int64(fileSize)
                    )
        )
        try Task.checkCancellation()
        resp = try await request(address: address, cmd: .dfuCheck, data: nil)
        if resp.data?.first != ASK_CODE_SUCCESS{
            throw BleError.dfuDataError(msg: "CMD.DFU_CHECK response error:\(resp.data?.hexEncodedString(separator: "_"))")
        }
        write(address: address, cmd: .dfuEnd, value: nil) { _ in
            
        }
        try Task.checkCancellation()
        callSink(event:
                    DFUStateEvent(
                        address: address,
                        state: DFUState.completed
                    )
        )
    }
}

